﻿using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Media;
using System.Windows.Controls;
using System.Windows.Shapes;


namespace Shrestha.Trigraph
{
    class TriGraphCanvas : Canvas, IObserver<WorkingSet>
    {
        #region Private Constants and Variables
        private const double TICKLENGTH = 5.0;
        private const double MARKERDIM = 4.0;

        private double FONTSIZEY, FONTSIZEX, FONTPADDINGX, FONTPADDINGY;
        private double canvasHeight, canvasWidth, px1, px2, px3, px4, py1, py2, py3, py4;
        private double latValueIncrement, lngValueIncrement, timeValueIncrement;
        private double graphHeight, graphWidth;
        private WorkingSet currentSet;
        private MainWindow mainWindow;

        private IDisposable unsubscriber;

        public void setParent(MainWindow m)
        {
            this.mainWindow = m;
        }

        #endregion

        public double maxLatitude { get; set; }
        public double minLatitude { get; set; }
        public double maxLongitude { get; set; }
        public double minLongitude { get; set; }
        public DateTime maxTime { get; set; }
        public DateTime minTime { get; set; }
        public bool showLatLngTicks { get; set; }
        public bool showTimeTicks { get; set; }
        public double timeTickInterval { get; set; }
        public double latitudeTickInterval { get; set; }
        public double longitudeTickInterval { get; set; }
        public double horizontalPadding { get; set; }
        public double verticalPadding { get; set; }

        private void DrawAxes()
        {
            #region Preprocessing
            InitializeDefault();
            canvasHeight = this.ActualHeight;
            canvasWidth = this.ActualWidth;
 
            double canvasY = 0.0;
            double canvasX = 0.0;

            ///Paddings up, down, right and left
            double upadding, dpadding, rpadding, lpadding;
            
            lpadding = rpadding = horizontalPadding;
            upadding = 5.0;
            dpadding = verticalPadding;
            
            px1 = canvasX + lpadding;
            py1 = canvasY + upadding;

            px2 = canvasX + lpadding;
            py2 = canvasY + (canvasHeight - dpadding);

            px3 = canvasX + (canvasWidth - rpadding);
            py3 = canvasY + (canvasHeight - dpadding);

            px4 = canvasX + (canvasWidth - rpadding);
            py4 = canvasY + upadding;

            Point p1 = new Point(px1, py1);
            Point p2 = new Point(px2, py2);
            Point p3 = new Point(px3, py3);
            Point p4 = new Point(px4, py4);

            PointCollection pc = new PointCollection();
            pc.Add(p1);
            pc.Add(p2);
            pc.Add(p3);
            pc.Add(p4);

            Polyline poly = new Polyline();
            poly.Points = pc;
            poly.StrokeThickness = 1;
            poly.Stroke = Brushes.Black;
            this.Children.Add(poly);

            graphHeight = canvasHeight - (upadding + dpadding);
            graphWidth = canvasWidth - (lpadding + rpadding);
            double tickLength = TICKLENGTH;

            latValueIncrement = maxLatitude - minLatitude;
            lngValueIncrement = maxLongitude - minLongitude;

            TimeSpan tsp = maxTime.Subtract(minTime);
            timeValueIncrement = tsp.TotalDays;

            #endregion
            #region Show Latitude and Longitude Ticks
            if (showLatLngTicks)
            {
                double latIncrement = latitudeTickInterval <= 0.0 ? 10 : latitudeTickInterval;
                double lngIncrement = longitudeTickInterval <= 0.0 ? 10 : longitudeTickInterval;
                
                int numberTicksLatitude = 0;
                int numberTicksLongitude = 0;

                try
                {
                    numberTicksLatitude = Convert.ToInt32(Math.Ceiling(graphHeight / latIncrement));
                    numberTicksLongitude = Convert.ToInt32(Math.Ceiling(graphHeight / lngIncrement));
                }
                catch (Exception e)
                {
                    Console.WriteLine(e.Message);
                }

                double latTextIncrement = latValueIncrement / numberTicksLatitude;
                double lngTextIncrement = lngValueIncrement / numberTicksLongitude;

                double tempLatFromX = px2;
                double tempLatFromY = py2;
                double tempLatToX = px2 - tickLength;
                double tempLatToY = py2;

                double tempExactMultiple = (FONTSIZEY + FONTPADDINGY) / latIncrement;
                int tempMultiple = Convert.ToInt32(Math.Ceiling(tempExactMultiple));
                
                for (int i = 0; i < numberTicksLatitude; i++)
                {
                    double tx = FONTPADDINGX;
                    double ty = tempLatFromY - (3*FONTSIZEY/4);

                    if (tempExactMultiple > 0 && ty > verticalPadding)
                    {
                        if ((i + 1) % tempMultiple == 0)
                        {
                            GraphText(tx, ty, String.Format("{0:0.0000}", minLatitude + latTextIncrement * i), this);
                        }
                    }
                    else
                    {
                        GraphText(tx, ty, String.Format("{0:0.0000}", minLatitude + latTextIncrement * i), this);
                    }

                    Line tickLatLine = new Line();
                    tickLatLine.StrokeThickness = 1;
                    tickLatLine.Stroke = Brushes.Black;
                    tickLatLine.X1 = tempLatFromX;
                    tickLatLine.Y1 = tempLatFromY;
                    tickLatLine.X2 = tempLatToX;
                    tickLatLine.Y2 = tempLatToY;
                    this.Children.Add(tickLatLine);

                    tempLatFromY = tempLatToY -= latIncrement;
                }

                double tempLngFromX = px3;
                double tempLngFromY = py3;
                double tempLngToX = px3 + tickLength;
                double tempLngToY = py3;

                for (int i = 0; i < numberTicksLongitude; i++)
                {
                    double tx = tempLngToX + FONTPADDINGX;
                    double ty = tempLngFromY - (3 * FONTSIZEY / 4);

                    if (tempExactMultiple > 0 && ty > verticalPadding)
                    {
                        if ((i + 1) % tempMultiple == 0)
                        {
                            GraphText(tx, ty, String.Format("{0:0.0000}", minLongitude + lngTextIncrement * i), this);
                        }
                    }
                    else
                    {
                        GraphText(tx, ty, String.Format("{0:0.0000}", minLongitude + lngTextIncrement * i), this);
                    }

                    Line tickLngLine = new Line();
                    tickLngLine.X1 = tempLngFromX;
                    tickLngLine.Y1 = tempLngFromY;
                    tickLngLine.X2 = tempLngToX;
                    tickLngLine.Y2 = tempLngToY;
                    tickLngLine.Stroke = Brushes.Black;
                    tickLngLine.StrokeThickness = 1;
                    this.Children.Add(tickLngLine);

                    tempLngFromY = tempLngToY -= lngIncrement;
                }
            }
            #endregion
            #region Show Time Ticks
            if (showTimeTicks)
            {
                double timeIncrement = timeTickInterval <= 0.0 ? 50 : timeTickInterval;
                int numberTicksTime = 0;

                double tempExactMultiple = (FONTSIZEX + FONTPADDINGX) / timeIncrement;
                int tempMultiple = Convert.ToInt32(Math.Ceiling(tempExactMultiple));

                try
                {
                    numberTicksTime = Convert.ToInt32(Math.Ceiling(graphWidth / timeIncrement));
                }
                catch (Exception e)
                {
                    Console.WriteLine(e.Message);
                }

                double tempTimeFromX = px2;
                double tempTimeFromY = py2;
                double tempTimeToX = px2;
                double tempTimeToY = py2 + tickLength;

                double timeTextIncrement = timeValueIncrement / numberTicksTime;

                for (int i = 0; i < numberTicksTime; i++)
                {
                    double tx = tempTimeFromX - FONTSIZEX/2;
                    double ty = tempTimeToY;

                    if (tempExactMultiple > 0 && tx > horizontalPadding)
                    {
                        if ((i + 1) % tempMultiple == 0)
                        {
                            TimeSpan ts = new TimeSpan(Convert.ToInt32(Math.Ceiling(timeTextIncrement * i)), 0, 0, 0);
                            //String s = minTime.Add(ts).ToString();
                            String s = minTime.Add(ts).Month.ToString() +"/"+ minTime.Add(ts).Year.ToString();

                            GraphText(tx, ty, s, this);
                        }
                    }
                    else
                    {
                        TimeSpan ts = new TimeSpan(Convert.ToInt32(Math.Ceiling(timeTextIncrement * i)), 0, 0, 0);
                        //String s = minTime.Add(ts).ToString();
                        String s = minTime.Add(ts).Month.ToString() + "/" + minTime.Add(ts).Year.ToString();
                        GraphText(tx, ty, s, this);
                    }

                    Line tickTimeLine = new Line();
                    tickTimeLine.Stroke = Brushes.Black;
                    tickTimeLine.StrokeThickness = 1;
                    tickTimeLine.X1 = tempTimeFromX;
                    tickTimeLine.Y1 = tempTimeFromY;
                    tickTimeLine.X2 = tempTimeToX;
                    tickTimeLine.Y2 = tempTimeToY;
                    this.Children.Add(tickTimeLine);

                    tempTimeFromX = tempTimeToX += timeIncrement;
                }
            }
            #endregion
        }

        private void InitializeDefault()
        {
            FONTSIZEY = FONTSIZEY <= 0.0 ? 10.0 : FONTSIZEY;
            FONTSIZEX = FONTSIZEX <= 0.0 ? 10.0 : FONTSIZEX;
            FONTPADDINGX = FONTPADDINGX <= 0.0 ? 5.0 : FONTPADDINGX;
            FONTPADDINGY = FONTPADDINGY <= 0.0 ? 2.0 : FONTPADDINGY;
        }

        private void GraphText(double x, double y, string text, Canvas c)
        {
            TextBlock textBlock = new TextBlock();
            textBlock.Text = text;
            textBlock.HorizontalAlignment = System.Windows.HorizontalAlignment.Right;
            textBlock.Foreground = Brushes.Black;
            textBlock.FontSize = FONTSIZEY;
            Canvas.SetLeft(textBlock, x);
            Canvas.SetTop(textBlock, y);
            
            c.Children.Add(textBlock);
        }

        private void GetCanvasCoordinates(double Latitude, double Longitude, DateTime EventDate, out double X, out double Y, out double X1, out double Y1, out double X2, out double Y2)
        {
            double pty1, ptx1, pty2, ptx2;
            double templat = ((Latitude - minLatitude) / (maxLatitude - minLatitude)) * graphHeight;
            Y1 = pty1 = py2 - templat;
            X1 = ptx1 = px2;

            double templng = ((Longitude - minLongitude) / (maxLongitude - minLongitude)) * graphHeight;
            Y2 = pty2 = py3 - templng;
            X2 = ptx2 = px3;

            double m = (pty2 - pty1) / graphWidth;

            double tempx = (EventDate.Subtract(minTime).TotalDays / maxTime.Subtract(minTime).TotalDays) * graphWidth;
            double tempy = m * tempx + pty1;

            X = tempx + ptx1;
            Y = tempy;
        }

        private void GetCanvasCoordinates(double Latitude, double Longitude, DateTime EventDate, out double X, out double Y)
        {
            double pty1, ptx1, pty2, ptx2;
            double templat = ((Latitude - minLatitude) / (maxLatitude - minLatitude)) * graphHeight;
            pty1 = py2 - templat;
            ptx1 = px2;

            double templng = ((Longitude - minLongitude) / (maxLongitude - minLongitude)) * graphHeight;
            pty2 = py3 - templng;
            ptx2 = px3;

            double m = (pty2 - pty1) / graphWidth;

            double tempx = (EventDate.Subtract(minTime).TotalDays / maxTime.Subtract(minTime).TotalDays) * graphWidth;
            double tempy = m * tempx + pty1;

            X = tempx + ptx1;
            Y = tempy;
        }

        public void Plot(TriEvent e, bool showLocationLines){
            
            double xcoordinate, ycoordinate, x1, y1, x2, y2;
            GetCanvasCoordinates(e.Latitude, e.Longitude, e.EventDate,out xcoordinate,out ycoordinate, out x1, out y1, out x2, out y2);
            if (showLocationLines)
            {
                try
                {
                    Line line = new Line
                    {
                        X1 = x1,
                        X2 = x2,
                        Y1 = y1,
                        Y2 = y2,
                        Stroke = Brushes.LightGray,
                        StrokeThickness = 0.8,
                        Opacity = 0.5
                    };
                    line.ToolTip = "Latitude: " + e.Latitude + ", Longitude: " + e.Longitude;
                    this.Children.Add(line);
                }
                catch (Exception someException)
                {
                    Console.WriteLine(someException.Message);
                }
            }

            TriMarker tm = new TriMarker(xcoordinate, ycoordinate, MARKERDIM, MARKERDIM, e, mainWindow);
            
            this.Children.Add(tm);
        }

        private void Plot(TriClusterEvent e, bool showLocationLines)
        {
            double x1, x2, x3, x4, y1, y2, y3, y4;
            double lx1, lx2, ly1, ly2;
            double lx3, lx4, ly3, ly4;

            if (e.MinLat == e.MaxLat && e.MinLng == e.MaxLng && e.MinDate == e.MaxDate)
            {
                GetCanvasCoordinates(e.MinLat, e.MinLng, e.MinDate, out x1, out y1, out lx1, out ly1, out lx2, out ly2);
                if (showLocationLines)
                {
                    try
                    {
                        Line line = new Line
                        {
                            X1 = lx1,
                            X2 = lx2,
                            Y1 = ly1,
                            Y2 = ly2,
                            Stroke = Brushes.LightGray,
                            StrokeThickness = 0.8,
                        };
                        line.ToolTip = "Latitude: " + e.MinLat + ", Longitude: " + e.MinLng;
                        this.Children.Add(line);
                    }
                    catch (Exception someException)
                    {
                        Console.WriteLine(someException.Message);
                    }
                }

                TriClusterMarker tm = new TriClusterMarker(x1, x1+MARKERDIM, x1, x1+MARKERDIM, y1, y1, y1+MARKERDIM,y1+MARKERDIM, e, mainWindow);

                this.Children.Add(tm);
            }
            else
            {
                GetCanvasCoordinates(e.MinLat, e.MinLng, e.MinDate, out x1, out y1, out lx1, out ly1, out lx2, out ly2);
                GetCanvasCoordinates(e.MinLat, e.MinLng, e.MaxDate, out x2, out y2);
                GetCanvasCoordinates(e.MaxLat, e.MaxLng, e.MinDate, out x3, out y3, out lx3, out ly3, out lx4, out ly4);
                GetCanvasCoordinates(e.MaxLat, e.MaxLng, e.MaxDate, out x4, out y4);

                if (showLocationLines)
                {
                    try
                    {
                        Line line1 = new Line
                        {
                            X1 = x1,
                            X2 = x2,
                            Y1 = y1,
                            Y2 = y2,
                            Stroke = Brushes.LightGray,
                            StrokeThickness = 0.8,
                        };
                        this.Children.Add(line1);

                        Line line2 = new Line
                        {
                            X1 = x3,
                            X2 = x4,
                            Y1 = y3,
                            Y2 = y4,
                            Stroke = Brushes.LightGray,
                            StrokeThickness = 0.8,
                        };
                        this.Children.Add(line2);
                    }
                    catch (Exception someException)
                    {
                        Console.WriteLine(someException.Message);
                    }
                }

                TriClusterMarker tm = new TriClusterMarker(x1, x2, x3, x4, y1, y2, y3, y4, e, mainWindow);
                this.Children.Add(tm);
            }
        }

        public void PlotCrumbs(List<TriEvent> le, bool showLocationLines, string crumbName)
        {
            Brush b = mainWindow.RandomBrush();
            TriEvent fromEvent = le[0];
            Plot(fromEvent, showLocationLines);
            TriEvent toEvent;
            for (int i = 1; i < le.Count; i++)
            {
                toEvent = le[i];
                Plot(le[i], showLocationLines);
                DrawCrumb(fromEvent, toEvent, b, crumbName);
                fromEvent = toEvent;
            }
        }

        private void DrawCrumb(TriEvent e1, TriEvent e2, Brush b,string crumbName)
        {
            double x1, y1, x2, y2;
            GetCanvasCoordinates(e1.Latitude, e1.Longitude, e1.EventDate, out x1, out y1);
            GetCanvasCoordinates(e2.Latitude, e2.Longitude, e2.EventDate, out x2, out y2);
            Line crumb = new Line{
                X1 = x1,
                X2 = x2,
                Y1 = y1,
                Y2 = y2,
                Stroke = b,
                StrokeThickness = 2,
                StrokeDashArray = {4, 1},
                ToolTip = "Storyline for "+crumbName
            };
            this.Children.Add(crumb);
        }

        private void Plot(List<TriEvent> le, bool showLocationLines)
        {
            foreach (TriEvent e in le)
            {
                Plot(e, showLocationLines);
            }
        }

        private void Plot(List<TriClusterEvent> le, bool showLocationLines)
        {
            foreach (TriClusterEvent e in le)
            {
                Plot(e, showLocationLines);
            }
        }

        private void Plot(List<WorkingSet> list, bool p)
        {
            foreach (WorkingSet ws in list)
            {
                if(ws!=null)
                PlotCrumbs(ws.TriEventList,p,ws.CrumbName);
            }
        }

        

        public void Draw(WorkingSet working)
        {
            this.currentSet = working;
            this.Children.Clear();
            horizontalPadding = 50.0;
            verticalPadding = 50.0;

            showLatLngTicks = true;
            latitudeTickInterval = 50;
            longitudeTickInterval = 50;

            showTimeTicks = true;
            timeTickInterval = 50;

            maxLatitude = currentSet.MaxDbLat;
            minLatitude = currentSet.MinDbLat;

            maxLongitude = currentSet.MaxDbLng;
            minLongitude = currentSet.MinDbLng;

            maxTime = currentSet.MaxDbDate;
            minTime = currentSet.MinDbDate;

            DrawAxes();

            if (currentSet.IsBreadCrumb)
            {
                Plot(currentSet.CrumbSet, currentSet.ShowLocationLines);
            }
            else
            {
                if (working.IsClustered)
                    Plot(currentSet.TriClusterEventList, currentSet.ShowLocationLines);
                else
                    Plot(currentSet.TriEventList, currentSet.ShowLocationLines);
            }
            
            mainWindow.UpdateWorkingSets(currentSet);
        }

        



        public void OnCompleted()
        {
            throw new NotImplementedException();
        }

        public void OnError(Exception error)
        {
            throw new NotImplementedException();
        }

        public void OnNext(WorkingSet value)
        {
            if (currentSet.CompareTo(value) != 0)
            {
                currentSet = value;
                Draw(value);
            }
        }

        public virtual void Subscribe(IObservable<WorkingSet> provider)
        {
            if (provider != null)
                unsubscriber = provider.Subscribe(this);
        }

        public virtual void Unsubscribe()
        {
            unsubscriber.Dispose();
        }

        public void GetExtents(Point m, Point n, out double minFilteredLat, out double maxFilteredLat, out double minFilteredLng, out double maxFilteredLng, out DateTime maxFilteredTime, out DateTime minFilteredTime)
        {
            double tmx = m.X < px1 ? px1 : m.X;
            double tmy = m.Y < py1 ? py1 : m.Y;
            double tnx = n.X > px3 ? px3 : n.X;
            double tny = n.Y > py3 ? py3 : n.Y;

            double modifier = ((tmy - py1) / (py3 - py1));
            minFilteredLat = modifier * latValueIncrement + minLatitude;
            minFilteredLng = modifier * lngValueIncrement + minLongitude;

            modifier = ((tny - py1) / (py3 - py1));
            maxFilteredLat = modifier * latValueIncrement + minLatitude;
            maxFilteredLng = modifier * lngValueIncrement + minLongitude;

            minFilteredTime = minTime.AddDays(((tmx - px1) / (px3 - px1)) * timeValueIncrement);
            maxFilteredTime = minTime.AddDays(((tnx - px1) / (px3 - px1)) * timeValueIncrement);
        }
    }
}
